stack exec: default to local emulator, add --cloud opt-out#1422
stack exec: default to local emulator, add --cloud opt-out#1422
Conversation
Local-first dev workflow: `stack exec` now signs in as the emulator admin via the well-known shared credentials and the run-dir PCK, falling back to the cloud only when --cloud is passed (or STACK_EXEC_DEFAULT_TARGET=cloud is set). Also: STACK_EMULATOR_*_PORT env vars take precedence over the legacy unprefixed names; emulator paths/ports/PCK polling extracted to lib/emulator-paths.ts; shared local-emulator admin creds hoisted to stack-shared so backend, dashboard auto-login, and CLI agree.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughThis PR centralizes local-emulator admin credentials in the shared package, adds emulator path/port utilities and PCK polling, implements a local-emulator auth flow with retries, refactors CLI auth helpers to be flag-less, adds cloud/local targeting for ChangesLocal Emulator CLI Support
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/stack-cli/src/lib/emulator-paths.ts (1)
79-91: ⚡ Quick winReplace
Date.now()withperformance.now()for elapsed-time measurement.
Date.now()is used here to compute a deadline and measure remaining time — exactly the use-case forperformance.now().As per coding guidelines: "Use
performance.now()instead ofDate.now()for measuring elapsed real time."
performanceis a global in Node.js 16+; if the project must support earlier Node, import it fromperf_hooks.♻️ Proposed fix
export async function pollInternalPck(timeoutMs: number): Promise<string | null> { const pckPath = internalPckPath(); - const deadline = Date.now() + timeoutMs; + const deadline = performance.now() + timeoutMs; let delay = 50; while (true) { try { const contents = readFileSync(pckPath, "utf-8").trim(); if (contents) return contents; } catch (e) { if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e; } - if (Date.now() >= deadline) return null; - const remaining = deadline - Date.now(); + if (performance.now() >= deadline) return null; + const remaining = deadline - performance.now(); await new Promise((r) => setTimeout(r, Math.min(delay, remaining))); delay = Math.min(delay * 2, 2000); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stack-cli/src/lib/emulator-paths.ts` around lines 79 - 91, Replace the uses of Date.now() used for elapsed-time/deadline calculation in the polling loop inside emulator-paths.ts (the code that sets deadline = Date.now() + timeoutMs and checks Date.now() >= deadline and computes remaining) with performance.now() to measure elapsed time; ensure you either use the global performance (Node 16+) or import { performance } from "perf_hooks" at the top if globals are not available, and update all references (deadline, remaining calculation, and delay scheduling) so they consistently use performance.now() while leaving readFileSync(pckPath, "utf-8") and the retry/backoff logic unchanged.packages/stack-cli/src/commands/init.ts (1)
274-287: 💤 Low valueDrop the unused
_flagsparameter from these handlers.
_flagsis no longer threaded into auth resolution, and both call sites inrunInitalready passflagsonly because the signature still demands it. Removing the parameter (and the correspondingflagsargument at the call sites) makes the obsolete data flow disappear instead of relying on the underscore convention.♻️ Proposed fix
-async function handleCreateCloud(_flags: Record<string, unknown>, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { +async function handleCreateCloud(opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { const sessionAuth = await ensureLoggedInSession(); ... } -async function handleLinkFromCloud(_flags: Record<string, unknown>, opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { +async function handleLinkFromCloud(opts: InitOptions, outputDir: string): Promise<{ configPath?: string }> { const sessionAuth = await ensureLoggedInSession(); ... }And update the call sites in
runInit(lines 130/140) to dropflags. Ifflagsis otherwise unused inrunInit, you can also drop theconst flags = program.opts();line.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stack-cli/src/commands/init.ts` around lines 274 - 287, Remove the unused _flags parameter from both handler signatures (handleCreateCloud and handleLinkFromCloud) and update their call sites in runInit to stop passing flags; specifically, delete the first parameter in each function definition and remove the corresponding argument where they are invoked (and if runInit no longer uses flags at all, also remove the const flags = program.opts() declaration). Ensure only these two function names are changed so auth resolution no longer receives obsolete flag data.packages/stack-cli/src/lib/auth.ts (1)
217-220: 💤 Low valueAvoid silent catch-all on
res.text().
.catch(() => "")swallows any failure (transport error mid-stream, abort, etc.) without observability. Since you're already inside an error-reporting branch, prefer explicit handling that surfaces the read failure in the resulting message instead of pretending the body was empty.♻️ Proposed fix
if (!res.ok) { - const body = await res.text().catch(() => ""); - throw new AuthError(`Local emulator sign-in failed (${res.status} ${res.statusText})${body ? `: ${body}` : ""}. Make sure the emulator is running with NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true.`); + let body = ""; + let bodyReadError: unknown = null; + try { + body = await res.text(); + } catch (err) { + bodyReadError = err; + } + const detail = body + ? `: ${body}` + : bodyReadError + ? ` (failed to read response body: ${bodyReadError instanceof Error ? bodyReadError.message : String(bodyReadError)})` + : ""; + throw new AuthError(`Local emulator sign-in failed (${res.status} ${res.statusText})${detail}. Make sure the emulator is running with NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true.`); }As per coding guidelines, "NEVER try-catch-all, NEVER void a promise, and NEVER use
.catch(console.error)." and "errors are never silently swallowed".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/stack-cli/src/lib/auth.ts` around lines 217 - 220, The current catch-all on res.text() silences read failures; change the logic around the failed response branch so you attempt to read the body but surface any body-read error in the thrown AuthError instead of returning an empty string. Concretely, wrap the await res.text() call in a try/catch that captures the thrown error (e.g., bodyReadError) and include either the body or the bodyReadError.message/stack in the new AuthError message (reference res, res.text(), and AuthError) so transport/stream errors are visible in logs rather than being silently swallowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/e2e/tests/general/cli.test.ts`:
- Around line 325-371: Add a guard assertion to both negative emulator tests
("local-default exec errors when emulator PCK file is missing" and
"local-default exec errors when emulator API is unreachable"): before calling
runCli, add expect(createdProjectId).toBeDefined() (the same check used in the
positive create-project test) so the tests fail fast if createdProjectId is
undefined and we don't mis-interpret a missing project ID as an emulator error;
update the tests that reference createdProjectId and runCli to include this
single-line guard.
In `@packages/stack-cli/src/lib/auth.ts`:
- Around line 179-202: Replace the wall-clock based timing in
localEmulatorSignInWithRetry with a monotonic clock: change all uses of
Date.now() that compute deadline, remainingForRequest, remaining, and the
deadline comparison to use performance.now() (keep units in milliseconds and the
same arithmetic with totalTimeoutMs), e.g. compute deadline = performance.now()
+ totalTimeoutMs and use performance.now() in the checks and mins for
perRequestTimeoutMs and sleep calculation; ensure lastError logic and thrown
message remain unchanged and no other behavior is altered.
---
Nitpick comments:
In `@packages/stack-cli/src/commands/init.ts`:
- Around line 274-287: Remove the unused _flags parameter from both handler
signatures (handleCreateCloud and handleLinkFromCloud) and update their call
sites in runInit to stop passing flags; specifically, delete the first parameter
in each function definition and remove the corresponding argument where they are
invoked (and if runInit no longer uses flags at all, also remove the const flags
= program.opts() declaration). Ensure only these two function names are changed
so auth resolution no longer receives obsolete flag data.
In `@packages/stack-cli/src/lib/auth.ts`:
- Around line 217-220: The current catch-all on res.text() silences read
failures; change the logic around the failed response branch so you attempt to
read the body but surface any body-read error in the thrown AuthError instead of
returning an empty string. Concretely, wrap the await res.text() call in a
try/catch that captures the thrown error (e.g., bodyReadError) and include
either the body or the bodyReadError.message/stack in the new AuthError message
(reference res, res.text(), and AuthError) so transport/stream errors are
visible in logs rather than being silently swallowed.
In `@packages/stack-cli/src/lib/emulator-paths.ts`:
- Around line 79-91: Replace the uses of Date.now() used for
elapsed-time/deadline calculation in the polling loop inside emulator-paths.ts
(the code that sets deadline = Date.now() + timeoutMs and checks Date.now() >=
deadline and computes remaining) with performance.now() to measure elapsed time;
ensure you either use the global performance (Node 16+) or import { performance
} from "perf_hooks" at the top if globals are not available, and update all
references (deadline, remaining calculation, and delay scheduling) so they
consistently use performance.now() while leaving readFileSync(pckPath, "utf-8")
and the retry/backoff logic unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d7d488bb-4abd-4f8e-b7d7-f09370f56e56
📒 Files selected for processing (17)
apps/backend/src/lib/local-emulator.tsapps/dashboard/src/app/(main)/(protected)/layout-client.tsxapps/e2e/tests/general/cli.test.tspackages/stack-cli/src/commands/emulator.test.tspackages/stack-cli/src/commands/emulator.tspackages/stack-cli/src/commands/exec.test.tspackages/stack-cli/src/commands/exec.tspackages/stack-cli/src/commands/init.tspackages/stack-cli/src/commands/login.tspackages/stack-cli/src/commands/project.tspackages/stack-cli/src/lib/app.tspackages/stack-cli/src/lib/auth.test.tspackages/stack-cli/src/lib/auth.tspackages/stack-cli/src/lib/config.tspackages/stack-cli/src/lib/emulator-paths.test.tspackages/stack-cli/src/lib/emulator-paths.tspackages/stack-shared/src/local-emulator.ts
Greptile SummaryThis PR makes
Confidence Score: 4/5Safe to merge after verifying the backend grants the emulator admin access to all local-emulator projects. The auth-flow refactor is well-structured and the unit/e2e test coverage is thorough. The two concerns worth a second look: (1) the positive e2e test signs in as the emulator admin but looks up a project created by the test user — if the backend's listOwnedProjects does not give the admin superuser project access, that test will always fail in local-emulator CI; (2) the per-phase timeout budget is applied independently to PCK polling and sign-in, so the actual wait can silently reach double the configured value, and the 0ms fast-fail path still fires one 1ms-timeout network attempt that can produce an 'aborted' message rather than a connection-specific one. Neither blocks the cloud path or any existing functionality.
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant exec.ts
participant auth.ts
participant emulator-paths.ts
participant LocalEmulator
User->>exec.ts: stack exec [--cloud] "js"
exec.ts->>exec.ts: resolveExecTarget(opts, env)
alt "--cloud or STACK_EXEC_DEFAULT_TARGET=cloud"
exec.ts->>auth.ts: resolveAuth(flags)
auth.ts-->>exec.ts: ProjectAuthWithRefreshToken (cloud creds)
else default: local emulator
exec.ts->>auth.ts: resolveLocalEmulatorAuth(flags)
auth.ts->>emulator-paths.ts: pollInternalPck(readyTimeoutMs)
emulator-paths.ts-->>auth.ts: internalPck (or null - throw AuthError)
auth.ts->>LocalEmulator: POST /api/v1/auth/password/sign-in (retry loop)
LocalEmulator-->>auth.ts: refresh_token
auth.ts-->>exec.ts: ProjectAuthWithRefreshToken (local creds)
end
exec.ts->>exec.ts: getAdminProject(auth)
exec.ts->>LocalEmulator: listOwnedProjects - find projectId
exec.ts->>exec.ts: new AsyncFunction(stackServerApp, js)
exec.ts-->>User: JSON result or error
|
- The positive happy-path test minted a project owned by the test user's team, but the CLI signs in as the local-emulator admin whose listOwnedProjects() only returns LOCAL_EMULATOR_OWNER_TEAM_ID-owned projects. Mint the project via /internal/local-emulator/project so it shows up under the admin's team. - Surface stderr when the positive test exits non-zero so future regressions report the real CLI error instead of a bare exit-code mismatch. - Add expect(createdProjectId).toBeDefined() guards to the two negative emulator tests for parity with the positive test. - Use performance.now() instead of Date.now() for the local-emulator sign-in retry deadline so wall-clock skew can't break the loop.
# Conflicts: # packages/stack-cli/src/commands/init.ts
- Use performance.now() in pollInternalPck for the polling deadline so
wall-clock skew can't break the loop, mirroring the same change in
localEmulatorSignInWithRetry.
- Surface response-body read failures in resolveLocalEmulatorAuth instead
of swallowing them with .catch(() => ""). The original message
("Local emulator sign-in failed (status text)") loses all diagnostic
info when res.text() itself throws; now we throw an AuthError that
includes the read error.
Summary
stack execnow defaults to the local emulator: signs in as the well-known emulator admin (local-emulator@stack-auth.com) using the PCK from~/.stack/emulator/run/vm/internal-pck. Pass--cloud(or setSTACK_EXEC_DEFAULT_TARGET=cloud) to hit the cloud API instead.STACK_EMULATOR_{BACKEND,DASHBOARD,MINIO,INBUCKET,MOCK_OAUTH}_PORTenv vars take precedence over the legacy unprefixedEMULATOR_*_PORTnames; the unprefixed names are still forwarded torun-emulator.sh.packages/stack-cli/src/lib/emulator-paths.ts; hoist the shared local-emulator admin credentials intopackages/stack-shared/src/local-emulator.tsso backend, dashboard auto-login, and CLI all agree on a single source.flagsparameter fromresolveLoginConfig/resolveSessionAuth/performLogin.getInternalAppreadspublishableClientKeyfrom the auth object so the local-emulator path can substitute the dynamically-read PCK.Heads up — breaking change for existing scripts. Any CI or shell that currently runs
stack exec '<js>'withSTACK_PROJECT_ID+ a refresh token will now try the local emulator and fail unless it passes--cloudor setsSTACK_EXEC_DEFAULT_TARGET=cloud.Test plan
pnpm lintpassespnpm typecheckpassesresolveExecTarget,isRetryableFetchError,localEmulatorReadyTimeoutMs,pollInternalPck, and the STACK_-prefixed port resolutionapps/e2e/tests/general/cli.test.ts:local-default exec errors when emulator PCK file is missing(negative)local-default exec errors when emulator API is unreachable(negative)local-default exec runs against the local emulator backend(positive, gated onNEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true)--cloudstack emulator startthenstack exec "return 1+1"returns2against the local emulatorstack exec --cloud "return 1+1"against a logged-in cloud session returns2Summary by CodeRabbit
New Features
stack execadds a--cloudflag andSTACK_EXEC_DEFAULT_TARGETto choose cloud vs local emulatorTests
Refactor
Chores